/**
 * (c) Copyright, Moebius Solutions, Inc., 2012
 *
 *                        All Rights Reserved
 *
 * LICENSE: GPLv3
 */
package com.moesol.gwt.maps.client.controls;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.Widget;
import com.moesol.gwt.maps.client.MapView;

/**
 * Control for panning and zooming the map.
 */
public class MapPanZoomControl extends Composite {

	// encapsulates code testable with regular JUnit tests
	// TODO: move more testable code here.
	static class Presenter {
		double calculateDelta(int panButtonDimension, int eventRelativeCoord,
				int maxPanPixels) {
			final double halfPanButtonDimension = panButtonDimension / 2.0;

			return ((eventRelativeCoord / halfPanButtonDimension) - 1.0)
					* maxPanPixels;
		}
	}

	private static MapPanZoomControlUiBinder uiBinder = GWT
			.create(MapPanZoomControlUiBinder.class);

	interface MapPanZoomControlUiBinder extends
			UiBinder<Widget, MapPanZoomControl> {
	}

	private Presenter m_presenter = new Presenter();

	private MapView m_map;
	private boolean m_doingMapAction = false;

	private int m_maxPanPixels;
	private int m_millisBetweenConsecutiveActions;
	
	@UiField
	DivElement clickHighlight;

	@UiField
	HTML zoomInButton;

	@UiField
	HTML zoomOutButton;

	private Timer m_panButtonTimer = new Timer() {
		@Override
		public void run() {
			m_map.moveMapByPixels((int) m_dx, (int) m_dy);
		}
	};

	private Timer m_zoomInButtonTimer = new Timer() {
		@Override
		public void run() {
			m_map.zoomByFactor(1.05);
		}
	};

	private Timer m_zoomOutButtonTimer = new Timer() {
		@Override
		public void run() {
			// 0.952380952 = 1/1.05
			m_map.zoomByFactor(0.952380952);
		}
	};

	private boolean m_panning;
	
	/**
	 * @param map The map to plug this control into.
	 * @param maxPanPixels The maximum number of pixels to
	 * move the map each time a pan occurs. 
	 * @param millisBetweenPans The milliseconds between consecutive
	 * pans that occur while the mouse is down on the pan portion
	 * of the control.
	 */
	public void initPanVals( int maxPanPixels, int millisBetweenPans ) {
		m_maxPanPixels = maxPanPixels;
		m_millisBetweenConsecutiveActions = millisBetweenPans;	
	}
	
	public void setMapView( MapView map ){
		m_map = map;
	}
	
	public MapPanZoomControl( ) {
		initWidget(uiBinder.createAndBindUi(this));
	}

	/**
	 * @param map The map to plug this control into.
	 * @param maxPanPixels The maximum number of pixels to
	 * move the map each time a pan occurs. 
	 * @param millisBetweenPans The milliseconds between consecutive
	 * pans that occur while the mouse is down on the pan portion
	 * of the control.
	 */
	public MapPanZoomControl(MapView map, int maxPanPixels, int millisBetweenPans) {
		m_map = map;
		m_maxPanPixels = maxPanPixels;
		m_millisBetweenConsecutiveActions = millisBetweenPans;
		initWidget(uiBinder.createAndBindUi(this));
	}

	@UiHandler("panButton")
	public void onPanButtonMouseUp(MouseUpEvent e) {
		e.preventDefault();
		stopPanLoop();
	}

	@UiHandler("panButton")
	public void onPanButtonMouseOut(MouseOutEvent e) {
		e.preventDefault();
		stopPanLoop();
	}

	@UiHandler("panButton")
	public void onPanButtonMouseDown(MouseDownEvent e) {
		e.preventDefault();
		updateVelocity(e);
		startPanLoop();
	}

	@UiHandler("panButton")
	public void onPanButtonMouseMove(MouseMoveEvent e) {
		e.preventDefault();
		if (m_panning) {
			updateVelocity(e);
		}
	}

	@UiField
	HTML panButton;

	private double m_dx;

	private double m_dy;

	@SuppressWarnings("rawtypes")
	private void updateVelocity(MouseEvent e) {
		int eventRelativeX = e.getRelativeX(panButton.getElement());
		int eventRelativeY = e.getRelativeY(panButton.getElement());

		final int panButtonWidth = panButton.getOffsetWidth();
		final int panButtonHeight = panButton.getOffsetHeight();

		if (eventRelativeX > 0 && eventRelativeX < panButtonWidth
				&& eventRelativeY > 0 && eventRelativeY < panButtonHeight) {
			final Style clickHighlightStyle = clickHighlight.getStyle();
			clickHighlightStyle.setDisplay(Display.BLOCK);
			clickHighlightStyle.setLeft(
					eventRelativeX - (clickHighlight.getClientWidth() / 3),
					Unit.PX);
			clickHighlightStyle.setTop(
					eventRelativeY - (clickHighlight.getClientHeight() / 3),
					Unit.PX);

			m_dx = m_presenter.calculateDelta(panButtonWidth, eventRelativeX,
					m_maxPanPixels);
			m_dy = -m_presenter.calculateDelta(panButtonHeight, eventRelativeY,
					m_maxPanPixels);
		}
	}

	private void startPanLoop() {
		m_panning = true;
		m_panButtonTimer.run();
		m_panButtonTimer.scheduleRepeating(m_millisBetweenConsecutiveActions);
	}

	private void stopPanLoop() {
		final Style clickHighlightStyle = clickHighlight.getStyle();
		clickHighlightStyle.setDisplay(Display.NONE);
		m_panning = false;
		m_panButtonTimer.cancel();
	}

	@UiHandler("zoomInButton")
	public void onZoomInButtonMouseDown(MouseDownEvent e) {
		e.preventDefault();
		resetStyleNamesTo(zoomInButton, true, "map-PanZoomControlZoomInButtonMouse");
		startZoomLoop(m_zoomInButtonTimer);
	}

	private void startZoomLoop(Timer zoomTimer) {
		m_doingMapAction = true;
		zoomTimer.run();
		zoomTimer.scheduleRepeating(m_millisBetweenConsecutiveActions);
	}

	@UiHandler("zoomInButton")
	public void onZoomInButtonMouseUp(MouseUpEvent e) {
		e.preventDefault();
		resetStyleNamesTo(zoomInButton, true, "map-PanZoomControlZoomInButtonMouseOver");
		stopZoomLoop(m_zoomInButtonTimer);
	}

	private void stopZoomLoop(Timer zoomTimer) {
		zoomTimer.cancel();
		if ( m_doingMapAction ){
			m_doingMapAction = false;
			m_map.updateView();
		}
	}

	@UiHandler("zoomInButton")
	public void onZoomInButtonMouseOver(MouseOverEvent e) {
		e.preventDefault();
		resetStyleNamesTo(zoomInButton, true, "map-PanZoomControlZoomInButtonMouseOver");
		stopZoomLoop(m_zoomInButtonTimer);
	}
        
	@UiHandler("zoomInButton")
	public void onZoomInButtonMouseOut(MouseOutEvent e) {
		e.preventDefault();
		resetStyleNamesTo(zoomInButton, true, "map-PanZoomControlZoomInButtonMouse");
		stopZoomLoop(m_zoomInButtonTimer);
	}

	@UiHandler("zoomOutButton")
	public void onZoomOutButtonMouseDown(MouseDownEvent e) {
		e.preventDefault();
		resetStyleNamesTo(zoomOutButton, false, "map-PanZoomControlZoomOutButtonMouse");
		startZoomLoop(m_zoomOutButtonTimer);
	}

	@UiHandler("zoomOutButton")
	public void onZoomOutButtonMouseUp(MouseUpEvent e) {
		e.preventDefault();
		resetStyleNamesTo(zoomOutButton, false, "map-PanZoomControlZoomOutButtonMouseOver");
		stopZoomLoop(m_zoomOutButtonTimer);
	}

	@UiHandler("zoomOutButton")
	public void onZoomOutButtonMouseOut(MouseOutEvent e) {
		e.preventDefault();
		resetStyleNamesTo(zoomOutButton, false, "map-PanZoomControlZoomOutButtonMouse");
		stopZoomLoop(m_zoomOutButtonTimer);
	}
        
	@UiHandler("zoomOutButton")
	public void onZoomOutButtonMouseOver(MouseOverEvent e) {
		e.preventDefault();
		resetStyleNamesTo(zoomOutButton, false, "map-PanZoomControlZoomOutButtonMouseOver");
		stopZoomLoop(m_zoomOutButtonTimer);
	}
        
    /**
     * Catch-all method for making sure only one button type is set on the element at any time.
     * This is the easiest necessary step to take cases such as the user hold-clicking on a button,
     * dragging onto the other button, and releasing their click. 
     * 
     * @param button
     * @param isInButton
     * @param newName 
     */
    private void resetStyleNamesTo(HTML button, boolean isInButton, String newName) {
        if (isInButton) {
            button.removeStyleName("map-PanZoomControlZoomInButtonMouse");
            button.removeStyleName("map-PanZoomControlZoomInButtonMouseOver");
        }
        else {
            button.removeStyleName("map-PanZoomControlZoomOutButtonMouse");
            button.removeStyleName("map-PanZoomControlZoomOutButtonMouseOver");
        }
        button.addStyleName(newName);
    }
}
